Skip to content

geolocation: add GeoProbe state and CRUD instructions#3120

Open
nikw9944 wants to merge 3 commits intomainfrom
bdz/doublezero-2952-part-2-geo-probe-crud
Open

geolocation: add GeoProbe state and CRUD instructions#3120
nikw9944 wants to merge 3 commits intomainfrom
bdz/doublezero-2952-part-2-geo-probe-crud

Conversation

@nikw9944
Copy link
Contributor

@nikw9944 nikw9944 commented Feb 27, 2026

Summary of Changes

  • Add GeoProbe account type to the Geolocation Program with CreateGeoProbe, UpdateGeoProbe, and DeleteGeoProbe instructions (foundation-gated via Serviceability CPI)
  • Create validates exchange is activated, IP is publicly routable, code length ≤32 bytes; delete enforces reference_count == 0
  • Add comprehensive IP validation that rejects all non-publicly-routable addresses (RFC 1918, loopback, multicast, link-local, reserved ranges, etc.)
  • Add GeoProbe PDA derivation (["doublezero", "probe", code.as_bytes()]) and extend serializer with try_acc_close for account deletion
  • Supports rfcs/rfc16-geolocation-verification.md

Diff Breakdown

Category Lines
Prod code ~500
Test code ~1000

Testing Verification

  • Unit tests and integration tests

@nikw9944 nikw9944 linked an issue Feb 27, 2026 that may be closed by this pull request
return Err(GeolocationError::InvalidServiceabilityProgramId.into());
}

// Verify it's a valid, activated Exchange account
Copy link
Contributor Author

@nikw9944 nikw9944 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI to reviewers: in RFC16 there's a to-do item to implement cross-program reference counting. We'll do it in a future PR to keep this PR more manageable.

@ben-dz ben-dz self-requested a review February 27, 2026 18:51
@nikw9944 nikw9944 force-pushed the bdz/doublezero-2952-part-2-geo-probe-crud branch from 27afe8a to df46177 Compare February 27, 2026 19:52
Add GeoProbe account type with CreateGeoProbe, UpdateGeoProbe, and
DeleteGeoProbe instructions. Includes IP validation (rejects all
non-publicly-routable addresses), code length validation, exchange
activation checks, and reference count protection on delete.

Reorder GeoProbe struct fields to place variable-length fields (code,
parent_devices) at the end for correct Borsh deserialization. Update
RFC16 to match and remove latency_threshold_ns.

Part 2 of 3 for RFC16 geolocation verification.
 - Add comprehensive integration tests for create, update, and delete GeoProbe operations
 - Test validation logic for IP addresses, code length, and exchange status
 - Test reference count protection on deletion
 - Update CLAUDE.md to require integration tests for all processors
 - Remove unused system_program parameter from update processor
@nikw9944 nikw9944 force-pushed the bdz/doublezero-2952-part-2-geo-probe-crud branch from df46177 to 1dfbf75 Compare February 27, 2026 20:59
…me environment where the program upgrade authority check expects the payer to match the authority stored in the program data account
@nikw9944 nikw9944 force-pushed the bdz/doublezero-2952-part-2-geo-probe-crud branch from 1dfbf75 to 083f58f Compare February 27, 2026 21:03
Copy link
Contributor

@karl-dz karl-dz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments

Comment on lines +94 to +97
if self.code.len() > 32 {
msg!("Code too long: {} bytes", self.code.len());
return Err(GeolocationError::InvalidCodeLength);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would never be validated, would it? In the create instruction, you validate the code length, so I can't imagine a GeoProbe would ever have a code > 32.

Plus find_program_address would have borked if this were >32 anyway, so this feels impossible given these two reasons

Comment on lines +98 to +105
if self.parent_devices.len() > MAX_PARENT_DEVICES {
msg!(
"Too many parent devices: {} (max {})",
self.parent_devices.len(),
MAX_PARENT_DEVICES
);
return Err(GeolocationError::MaxParentDevicesReached);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No instructions add parent devices yet. But I'm wondering if this would ever hit, too? I'm guessing when an instruction that introduces adding parent devices, there would be validation of whether this violates MAX_PARENT_DEVICES, so this condition would never be true.

Curious about a scenario where this would be true.

Comment on lines +88 to +108
impl Validate for GeoProbe {
fn validate(&self) -> Result<(), GeolocationError> {
if self.account_type != AccountType::GeoProbe {
msg!("Invalid account type: {}", self.account_type);
return Err(GeolocationError::InvalidAccountType);
}
if self.code.len() > 32 {
msg!("Code too long: {} bytes", self.code.len());
return Err(GeolocationError::InvalidCodeLength);
}
if self.parent_devices.len() > MAX_PARENT_DEVICES {
msg!(
"Too many parent devices: {} (max {})",
self.parent_devices.len(),
MAX_PARENT_DEVICES
);
return Err(GeolocationError::MaxParentDevicesReached);
}
Ok(())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I've written the two comments above, I'm wondering why this trait even needs to be implemented with any of the accounts you have in this smart contract. What does calling validate protect against exactly?

Comment on lines +21 to +59
#[test]
fn test_program_config_pda_is_deterministic() {
let program_id = test_program_id();
let (pda1, bump1) = get_program_config_pda(&program_id);
let (pda2, bump2) = get_program_config_pda(&program_id);
assert_eq!(pda1, pda2);
assert_eq!(bump1, bump2);
}

#[test]
fn test_program_config_pda_differs_by_program_id() {
let (pda1, _) = get_program_config_pda(&Pubkey::new_unique());
let (pda2, _) = get_program_config_pda(&Pubkey::new_unique());
assert_ne!(pda1, pda2);
}

#[test]
fn test_geo_probe_pda_is_deterministic() {
let program_id = test_program_id();
let (pda1, bump1) = get_geo_probe_pda(&program_id, "probe-a");
let (pda2, bump2) = get_geo_probe_pda(&program_id, "probe-a");
assert_eq!(pda1, pda2);
assert_eq!(bump1, bump2);
}

#[test]
fn test_geo_probe_pda_differs_by_code() {
let program_id = test_program_id();
let (pda1, _) = get_geo_probe_pda(&program_id, "probe-a");
let (pda2, _) = get_geo_probe_pda(&program_id, "probe-b");
assert_ne!(pda1, pda2);
}

#[test]
fn test_geo_probe_pda_differs_by_program_id() {
let (pda1, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a");
let (pda2, _) = get_geo_probe_pda(&Pubkey::new_unique(), "probe-a");
assert_ne!(pda1, pda2);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests don't seem necessary (and would argue the one below this comment isn't either, too). What do you think?

Comment on lines +66 to +76
let probe = GeoProbe::try_from(&probe_account.data[..]).unwrap();

assert_eq!(probe.account_type, AccountType::GeoProbe);
assert_eq!(probe.owner, payer.pubkey());
assert_eq!(probe.exchange_pk, exchange_pubkey);
assert_eq!(probe.public_ip, Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(probe.location_offset_port, 4242);
assert_eq!(probe.metrics_publisher_pk, args.metrics_publisher_pk);
assert_eq!(probe.reference_count, 0);
assert_eq!(probe.code, code);
assert_eq!(probe.parent_devices.len(), 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, if GeoProbe derives PartialEq then it would be better to assert_eq!(probe, expected_probe) in case you add more fields to your geo probe account in the future. With the way this is written, you run the risk of missing asserting that the new fields will be equal what you expect

);

let result = banks_client.process_transaction(tx).await;
assert!(result.is_err());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not seem necessary since you unwrap the error later on. Should the claude markdown be informed to not perform this is_err check?

Comment on lines +172 to +173
assert!(result.is_err());
// Exchange not activated should return InvalidAccountData
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to assert that the error equals something specific?

Comment on lines +251 to +253
// Verify immutable fields didn't change
assert_eq!(probe.code, code);
assert_eq!(probe.exchange_pk, exchange_pubkey);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the assertion that immutable fields don't change. But like the comment above, might be better to assert full struct equality in case more immutable fields get added


1. **Assert specific errors**: Tests should assert specific error types (e.g., `ProgramError::Custom(17)`), not just `.is_err()`. This catches regressions where the instruction fails for the wrong reason.

2. **Don't test framework functionality**: Avoid writing tests that only exercise SDK/framework behavior (e.g., testing that `Pubkey::find_program_address` is deterministic or produces different outputs for different inputs). Focus tests on your program's logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess when the unit tests were generated in this PR, claude forgot about this


### Testing

1. **Assert specific errors**: Tests should assert specific error types (e.g., `ProgramError::Custom(17)`), not just `.is_err()`. This catches regressions where the instruction fails for the wrong reason.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

geolocation: smartcontract program part 2 - geoprobe

2 participants